package ch.ethz.fcl.metrobuzz.data.transit.bus;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.ethz.fcl.metrobuzz.data.transit.Stop;
import ch.ethz.fcl.metrobuzz.data.transit.TransitRoute;
import ch.ethz.fcl.metrobuzz.tools.MBTools;

/**
 * Contains bus arriving and departing time at each stop along the transit line
 * retrieved from the raw data
 * 
 * @author ZengWei
 * 
 */
public class RawBusSchedule {
	private TransitRoute route; // transit line the bus runs
	private int busRegNum; // bus registration number

	// note one bus may come several times
	private Map<Stop, List<WeightedTime>> tapInTable; // tap in time table
	private Map<Stop, List<WeightedTime>> tapOutTable; // tap out time table

	private List<BusPair> pairs;
	private CalibratedBusSchedule calSchedule;

	private WeightedTimeComparator comp = new WeightedTimeComparator();

	public RawBusSchedule(TransitRoute route, int busNum) {
		this.route = route;
		this.busRegNum = busNum;
		tapInTable = new HashMap<Stop, List<WeightedTime>>();
		tapOutTable = new HashMap<Stop, List<WeightedTime>>();

		pairs = new ArrayList<BusPair>();
		calSchedule = new CalibratedBusSchedule(this);
	}

	public void addTapInTime(Stop stop, float tapTime) {
		List<WeightedTime> existArrTimes = tapInTable.get(stop);
		if (existArrTimes == null) { // if no tap yet
			existArrTimes = new ArrayList<WeightedTime>();
			WeightedTime wt = new WeightedTime(1, tapTime);
			existArrTimes.add(wt);
			Collections.sort(existArrTimes, comp);
			tapInTable.put(stop, existArrTimes);
		} else {
			int index = -1;
			for (int i = 0; i < existArrTimes.size(); i++) {
				WeightedTime arrTime = existArrTimes.get(i);
				// 10 minutes consider same bus at same time
				if (Math.abs(arrTime.time - tapTime) < 10 * 60) {
					index = i;
					break;
				}
			}

			// find time the bus at this stop
			if (index != -1) {
				WeightedTime wt = existArrTimes.get(index);
				wt.time = (wt.time * wt.weight + tapTime) / (wt.weight + 1);
				wt.weight++;
			} else {
				WeightedTime wt = new WeightedTime(1, tapTime);
				existArrTimes.add(wt);
			}
			Collections.sort(existArrTimes, comp);
		}
	}

	public void addTapOutTime(Stop stop, float tapTime) {
		List<WeightedTime> existDepTimes = tapOutTable.get(stop);
		if (existDepTimes == null) { // if no tap yet
			existDepTimes = new ArrayList<WeightedTime>();
			WeightedTime wt = new WeightedTime(1, tapTime);
			existDepTimes.add(wt);
			Collections.sort(existDepTimes, comp);
			tapOutTable.put(stop, existDepTimes);
		} else {
			int index = -1;
			for (int i = 0; i < existDepTimes.size(); i++) {
				WeightedTime arrTime = existDepTimes.get(i);
				// 10 minutes consider same bus at same time
				if (Math.abs(arrTime.time - tapTime) < 10 * 60) {
					index = i;
					break;
				}
			}

			// find time the bus at this stop
			if (index != -1) {
				WeightedTime wt = existDepTimes.get(index);
				wt.time = (wt.time * wt.weight + tapTime) / (wt.weight + 1);
				wt.weight++;
			} else {
				WeightedTime wt = new WeightedTime(1, tapTime);
				existDepTimes.add(wt);
			}
			Collections.sort(existDepTimes, comp);
		}
	}

	public void cleanup() {
		findBusPairs();
		calSchedule.cleanup();
	}

	/**
	 * for each transit, find the departing (first tap in) and arriving (last
	 * tap out) times and stops
	 */
	private void findBusPairs() {
		List<Stop> stops = route.getStops();
		List<WeightedTime> startTapInTimes = tapInTable.get(stops.get(0));
		List<WeightedTime> endTapOutTimes = tapOutTable.get(stops.get(stops
				.size() - 1));

		List<Float> times = new ArrayList<Float>();
		if (startTapInTimes != null)
			for (WeightedTime startTapInTime : startTapInTimes)
				times.add(startTapInTime.time);
		if (endTapOutTimes != null)
			for (WeightedTime endTapOutTime : endTapOutTimes)
				times.add(endTapOutTime.time);

		for (int i = 1; i < stops.size() - 1; i++) {
			List<Float> tapTimes = getTapTime(stops.get(i));
			if (tapTimes != null)
				times.addAll(tapTimes);
		}

		Collections.sort(times);
		if (times.size() == 0)
			return;

		List<BusPair> tempPairs = new ArrayList<BusPair>();

		int index = 0;
		int pairStartStopIndex = 0, lastStopIndex = 0, currentStopIndex = 0;

		Float startTime = times.get(0);
		pairStartStopIndex = getTimeInStopIndex(startTime);

		Float curTime = times.get(0);
		Float lastTime = curTime;
		while (index < times.size()) {
			curTime = times.get(index);
			currentStopIndex = getTimeInStopIndex(curTime);

			// new bus transit starts
			if (currentStopIndex < lastStopIndex) {
				BusPair pair = new BusPair(stops.get(pairStartStopIndex),
						startTime, stops.get(lastStopIndex), lastTime);

				tempPairs.add(pair);

				startTime = curTime;
				pairStartStopIndex = currentStopIndex;
			}

			lastTime = curTime;
			lastStopIndex = currentStopIndex;
			index++;
		}

		BusPair pair = new BusPair(stops.get(pairStartStopIndex), startTime,
				stops.get(lastStopIndex), lastTime);

		tempPairs.add(pair);

		// remove pairs that has only one stop since we cannot esitimate other
		// stops schedule
		for (int i = 0; i < tempPairs.size(); i++) {
			BusPair p = tempPairs.get(i);
			// if (route.getId().equals("190_weekday_1") && busRegNum == 1119)
			// System.out.println("before remove" + pairs.size());
			if (!p.getFirstTapIn().getId().equals(p.getLastTapOut().getId())
					|| p.getStartTime() != p.getEndTime()) {
				pairs.add(p);
				// if (route.getId().equals("190_weekday_1") && busRegNum ==
				// 1119)
				// System.out.println(i + " th Removed " + p.toString());
			}
			// if (route.getId().equals("190_weekday_1") && busRegNum == 1119)
			// System.out.println("after remove" + pairs.size());
		}
	}

	public int getTimeInStopIndex(Float time) {
		List<Stop> stops = route.getStops();
		List<WeightedTime> startTapInTimes = tapInTable.get(stops.get(0));
		List<WeightedTime> endTapOutTimes = tapOutTable.get(stops.get(stops
				.size() - 1));

		if (startTapInTimes != null && match(startTapInTimes, time))
			return 0;
		else if (endTapOutTimes != null && match(endTapOutTimes, time))
			return stops.size() - 1;
		else {
			int index = 0;
			for (int i = 1; i < stops.size() - 1; i++) {
				List<Float> tapTimes = getTapTime(stops.get(i));
				if (tapTimes != null && tapTimes.contains(time)) {
					index = i;
					break;
				}
			}
			return index;
		}
	}

	private boolean match(List<WeightedTime> wts, float time) {
		if (wts != null)
			for (WeightedTime wt : wts)
				if (wt.time == time)
					return true;

		return false;
	}

	public int getBusNum() {
		return busRegNum;
	}

	public TransitRoute getRoute() {
		return route;
	}

	public List<Float> getTapInTime(Stop stop) {
		List<WeightedTime> wts = tapInTable.get(stop);

		List<Float> times = new ArrayList<Float>();
		if (wts != null)
			for (WeightedTime wt : wts)
				times.add(wt.time);

		return times;
	}

	public List<Float> getTapOutTime(Stop stop) {
		List<WeightedTime> wts = tapOutTable.get(stop);

		List<Float> times = new ArrayList<Float>();
		if (wts != null)
			for (WeightedTime wt : wts)
				times.add(wt.time);
		return times;
	}

	public List<BusPair> getBusPairs() {
		return pairs;
	}

	public CalibratedBusSchedule getCalibratedSchedule() {
		return calSchedule;
	}

	public List<Float> getTapTime(Stop stop) {
		List<WeightedTime> tapInTimes = tapInTable.get(stop);
		List<WeightedTime> tapOutTimes = tapOutTable.get(stop);

		List<Float> tapTimes = new ArrayList<Float>();

		if (tapInTimes != null)
			for (WeightedTime time : tapInTimes)
				tapTimes.add(time.time);

		if (tapOutTimes != null)
			for (WeightedTime wt : tapOutTimes) {
				boolean exist = false;
				for (Float inTime : tapTimes) {
					if (Math.abs(inTime - wt.time) < 10 * 60) {
						exist = true;
						break;
					}
				}

				if (!exist)
					tapTimes.add(wt.time);
			}

		Collections.sort(tapTimes);
		return tapTimes;
	}

	private String getTimes(List<Float> times) {
		String timesInString = "";

		if (times != null)
			for (int i = 0; i < times.size(); i++) {
				Float time = times.get(i);
				timesInString += MBTools.formatTimeInSeconds(time);
				if (i != times.size() - 1)
					timesInString += "/";
			}

		return timesInString;
	}

	@Override
	public String toString() {
		String out = "Bus " + busRegNum + " in line " + route.getId()
				+ " pairs size " + pairs.size() + ":\n";
		List<Stop> stops = route.getStops();
		for (int i = 0; i < stops.size(); i++) {
			Stop stop = stops.get(i);

			List<Float> tapTimes = this.getTapTime(stop);
			if (i == 0)
				tapTimes = this.getTapInTime(stop);
			else if (i == stops.size() - 1)
				tapTimes = this.getTapOutTime(stop);

			out += "Stop " + stop.getId() + " arrive at: " + getTimes(tapTimes)
					+ "\n";
		}

		for (BusPair p : pairs)
			out += p.getFirstTapIn().getId()
					+ " at "
					+ MBTools.formatTimeInSeconds(p.getStartTime())
					+ "->"
					+ p.getLastTapOut().getId()
					+ " at "
					+ MBTools.formatTimeInSeconds(p.getEndTime())
					+ " time "
					+ MBTools.formatTimeInSeconds(p.getEndTime()
							- p.getStartTime()) + "\n";

		return out;
	}

	static class WeightedTime {
		int weight;
		float time;

		public WeightedTime() {
			this.weight = 1;
			this.time = 0;
		}

		public WeightedTime(int weight, float time) {
			this.weight = weight;
			this.time = time;
		}
	}

	class WeightedTimeComparator implements Comparator<WeightedTime> {
		@Override
		public int compare(WeightedTime wt1, WeightedTime wt2) {
			if (wt1.time > wt2.time)
				return 1;
			else if (wt1.time < wt2.time)
				return -1;
			return 0;
		}

	}
}
